/*
 * Copyright (C) 2011 Christopher Probst
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of the 'FoxNet Codec' nor the names of its 
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.foxnet.codec.impl;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import com.foxnet.codec.Cache;
import com.foxnet.codec.CachePolicy;
import com.foxnet.codec.Codec;
import com.foxnet.codec.CodecRegistry;
import com.foxnet.codec.ObjectOutputChannel;

/**
 * @author Christopher Probst
 */
abstract class AbstractOStream<R> extends AbstractIoStream<R> implements
	ObjectOutputChannel<R> {

    /*
     * This is a map which maps primitive classes (including their wrapper
     * classes) to protocol ids.
     */
    private static final Map<Class<?>, Integer> PRIMITIVE_TYPE_CLASSES;

    static {

	// Create and fill primitive type class map
	Map<Class<?>, Integer> primitiveTypeClasses =
		new HashMap<Class<?>, Integer>();

	// Primitive type classes
	primitiveTypeClasses.put(Byte.class, TC_BYTE);
	primitiveTypeClasses.put(byte.class, TC_BYTE);
	primitiveTypeClasses.put(Boolean.class, TC_BOOLEAN);
	primitiveTypeClasses.put(boolean.class, TC_BOOLEAN);
	primitiveTypeClasses.put(Character.class, TC_CHAR);
	primitiveTypeClasses.put(char.class, TC_CHAR);
	primitiveTypeClasses.put(Short.class, TC_SHORT);
	primitiveTypeClasses.put(short.class, TC_SHORT);
	primitiveTypeClasses.put(Integer.class, TC_INT);
	primitiveTypeClasses.put(int.class, TC_INT);
	primitiveTypeClasses.put(Long.class, TC_LONG);
	primitiveTypeClasses.put(long.class, TC_LONG);
	primitiveTypeClasses.put(Float.class, TC_FLOAT);
	primitiveTypeClasses.put(float.class, TC_FLOAT);
	primitiveTypeClasses.put(Double.class, TC_DOUBLE);
	primitiveTypeClasses.put(double.class, TC_DOUBLE);

	// Save as unmodifiable map
	PRIMITIVE_TYPE_CLASSES =
		Collections.unmodifiableMap(primitiveTypeClasses);
    }

    // The cache of this output
    private final Cache cache;
    // Used to clear the cache
    private boolean clearCache;
    // Used to determine whether or not to cache an object
    private boolean isCacheObject;
    // The cache policy of this output!
    private CachePolicy cachePolicy;
    // Used to write serializable objects
    private ObjectOutputStream lazyObjectOutputStream;
    // The recursive depth
    private int recursiveDepth;

    private void writeSerializableObject(Object object) throws IOException {
	// Write flag
	writeTypeClass(TC_SERIALIZABLE_OBJECT);

	if (lazyObjectOutputStream == null) {
	    lazyObjectOutputStream = new ObjectOutputStream(new OutputStream() {

		@Override
		public void write(int b) throws IOException {
		    writeByte(b);
		}

		/*
		 * (non-Javadoc)
		 * @see java.io.OutputStream#write(byte[], int, int)
		 */
		@Override
		public void write(byte[] b, int off, int len)
			throws IOException {
		    writeBuffer(ByteBuffer.wrap(b, off, len));
		}
	    });
	}

	// Write the object
	lazyObjectOutputStream.writeUnshared(object);

	// Flush the stream
	lazyObjectOutputStream.flush();
    }

    @SuppressWarnings("unchecked")
    private <T> void writeOrdinaryObject(T object, Codec<?>[] codecs)
	    throws Exception {
	// Write flag
	writeTypeClass(TC_OBJECT);

	// Get the name of the object class
	String name = object.getClass().getName();

	// Write the class name
	writeObject(name);

	for (Codec<?> codec : codecs) {
	    // Encode object
	    if (((Codec<T>) codec).encode(this, object)) {
		break;
	    }
	}
    }

    private void writeUnknownObject(Object object) throws Exception {

	// Get hierarchy codecs
	Codec<?>[] codecs = getHierarchyCodecs(object.getClass());

	if (codecs.length > 0) {

	    // Write as ordinary object
	    writeOrdinaryObject(object, codecs);

	} else {
	    /*
	     * If there were no codec we have to use the Java serialization if
	     * possible!
	     */
	    if (object instanceof Serializable) {

		// Write serializable object
		writeSerializableObject(object);
	    } else {
		throw new IOException("There are no bound codecs for " + object
			+ ". In addition: The object does not implement "
			+ "java.io.Serializable, either.");
	    }
	}
    }

    private void writePrimitiveObject(Object primitiveObject,
	    int primitiveObjectId) throws IOException {

	// Get primitive id and switch
	switch (primitiveObjectId) {
	case TC_BYTE:
	    writeUncachedTypeClass(TC_BYTE);
	    writeByte((Byte) primitiveObject);
	    break;
	case TC_BOOLEAN:
	    writeUncachedTypeClass(TC_BOOLEAN);
	    writeBoolean((Boolean) primitiveObject);
	    break;
	case TC_CHAR:
	    writeUncachedTypeClass(TC_CHAR);
	    writeChar((Character) primitiveObject);
	    break;
	case TC_SHORT:
	    writeUncachedTypeClass(TC_SHORT);
	    writeShort((Short) primitiveObject);
	    break;
	case TC_INT:
	    writeUncachedTypeClass(TC_INT);
	    writeInt((Integer) primitiveObject);
	    break;
	case TC_LONG:
	    writeUncachedTypeClass(TC_LONG);
	    writeLong((Long) primitiveObject);
	    break;
	case TC_FLOAT:
	    writeUncachedTypeClass(TC_FLOAT);
	    writeFloat((Float) primitiveObject);
	    break;
	case TC_DOUBLE:
	    writeUncachedTypeClass(TC_DOUBLE);
	    writeDouble((Double) primitiveObject);
	    break;
	}
    }

    private void writePrimitiveArray(Object primitiveArrayObject,
	    int primitiveObjectId) throws IOException {

	// Get the length of the array
	writeInt(Array.getLength(primitiveArrayObject));

	// Get primitive id and switch
	switch (primitiveObjectId) {
	case TC_BYTE:
	    writeTypeClass(TC_BYTE_ARRAY);
	    writeBuffer(ByteBuffer.wrap((byte[]) primitiveArrayObject));
	    break;
	case TC_BOOLEAN:
	    writeTypeClass(TC_BOOLEAN_ARRAY);
	    boolean[] booleans = (boolean[]) primitiveArrayObject;
	    for (boolean booleanValue : booleans) {
		writeBoolean(booleanValue);
	    }
	    break;
	case TC_CHAR:
	    writeTypeClass(TC_CHAR_ARRAY);
	    writeBuffer(CharBuffer.wrap((char[]) primitiveArrayObject));
	    break;
	case TC_SHORT:
	    writeTypeClass(TC_SHORT_ARRAY);
	    writeBuffer(ShortBuffer.wrap((short[]) primitiveArrayObject));
	    break;
	case TC_INT:
	    writeTypeClass(TC_INT_ARRAY);
	    writeBuffer(IntBuffer.wrap((int[]) primitiveArrayObject));
	    break;
	case TC_LONG:
	    writeTypeClass(TC_LONG_ARRAY);
	    writeBuffer(LongBuffer.wrap((long[]) primitiveArrayObject));
	    break;
	case TC_FLOAT:
	    writeTypeClass(TC_FLOAT_ARRAY);
	    writeBuffer(FloatBuffer.wrap((float[]) primitiveArrayObject));
	    break;
	case TC_DOUBLE:
	    writeTypeClass(TC_DOUBLE_ARRAY);
	    writeBuffer(DoubleBuffer.wrap((double[]) primitiveArrayObject));
	    break;
	}
    }

    private int getBufferTypeClass(Buffer bufferObject) {
	if (bufferObject instanceof ByteBuffer) {
	    return TC_BYTE_BUFFER;
	} else if (bufferObject instanceof CharBuffer) {
	    return TC_CHAR_BUFFER;
	} else if (bufferObject instanceof ShortBuffer) {
	    return TC_SHORT_BUFFER;
	} else if (bufferObject instanceof IntBuffer) {
	    return TC_INT_BUFFER;
	} else if (bufferObject instanceof LongBuffer) {
	    return TC_LONG_BUFFER;
	} else if (bufferObject instanceof FloatBuffer) {
	    return TC_FLOAT_BUFFER;
	} else {
	    return TC_DOUBLE_BUFFER;
	}
    }

    private void writeBufferObject(Buffer bufferObject) throws IOException {

	// Get buffer type class
	int bufferTypeClass = getBufferTypeClass(bufferObject);

	// Write the buffer type class
	writeTypeClass(bufferTypeClass);

	// Write the length
	writeInt(bufferObject.remaining());

	// Write the buffer
	writeDefinedBuffer(bufferObject, bufferTypeClass);
    }

    private void writeDefinedBuffer(Buffer buffer, int bufferTypeClass)
	    throws IOException {
	switch (bufferTypeClass) {
	case TC_BYTE_BUFFER:
	    writeByteBuffer((ByteBuffer) buffer);
	    break;
	case TC_CHAR_BUFFER:
	    writeCharBuffer((CharBuffer) buffer);
	    break;
	case TC_SHORT_BUFFER:
	    writeShortBuffer((ShortBuffer) buffer);
	    break;
	case TC_INT_BUFFER:
	    writeIntBuffer((IntBuffer) buffer);
	    break;
	case TC_LONG_BUFFER:
	    writeLongBuffer((LongBuffer) buffer);
	    break;
	case TC_FLOAT_BUFFER:
	    writeFloatBuffer((FloatBuffer) buffer);
	    break;
	case TC_DOUBLE_BUFFER:
	    writeDoubleBuffer((DoubleBuffer) buffer);
	    break;
	}
    }

    private void writeObjectArray(Object arrayObject) throws Exception {

	// Try to get the real array type
	Class<?> arrayType = arrayObject.getClass().getComponentType();

	// 1 means that this is the last array
	int dimsLeft = 1;

	// Go through for all dimensions left
	while (arrayType.isArray()) {
	    // Get next
	    arrayType = arrayType.getComponentType();

	    // Increase
	    dimsLeft++;
	}

	// Write type constant
	writeTypeClass(TC_OBJECT_ARRAY);

	// Convert to real array for fast access
	Object[] realArray = (Object[]) arrayObject;

	// Put dimensions
	writeByte(dimsLeft);

	// Write length
	writeInt(realArray.length);

	// Write array component type
	writeObject(arrayType.getName());

	// For all items
	for (Object item : realArray) {

	    // Write next
	    writeObject(item);
	}
    }

    private void writeClass(Class<?> classObject) throws Exception {

	// Write type constant
	writeTypeClass(TC_CLASS);

	// Write enum class name
	writeObject(classObject.getName());
    }

    private void writeEnum(Enum<?> enumObject) throws Exception {

	// Write type constant
	writeTypeClass(TC_ENUM);

	// Write enum class name
	writeObject(enumObject.getClass().getName());

	// Write the ordinal of the enum
	writeInt(enumObject.ordinal());
    }

    private void writeString(String string) throws IOException {

	// Get string length
	int length = string.length();

	// Write string identifier
	writeTypeClass(TC_STRING);

	// Write string length
	writeInt(length);

	// Write all strings
	for (int i = 0, c; i < length; i++) {
	    // Get char
	    c = string.charAt(i);

	    // Write the char as utf
	    if (c <= 0x007F) {
		writeByte(c);
	    } else if (c > 0x07FF) {
		// buffer.put((byte) (0xE0 | c >> 12 & 0x0F));
		// buffer.put((byte) (0x80 | c >> 6 & 0x3F));
		// buffer.put((byte) (0x80 | c & 0x3F));

		writeByte(0xE0 | c >> 12 & 0x0F);
		writeShort((0x80 | c >> 6 & 0x3F) | ((0x80 | c & 0x3F) << 8));
	    } else {
		writeShort((0xC0 | c >> 6 & 0x1F) | ((0x80 | c & 0x3F) << 8));

		// buffer.put((byte) (0xC0 | c >> 6 & 0x1F));
		// buffer.put((byte) (0x80 | c & 0x3F));
	    }
	}
    }

    private void writeUncachedTypeClass(int tc) throws IOException {
	// Write type class!
	writeByte(tc);
    }

    private void writeTypeClass(int tc) throws IOException {

	/*
	 * If the object which we are going to write is a cache object we have
	 * take care of the "cache-flag" or "clear-cache-flag".
	 */
	if (isCacheObject) {
	    /*
	     * Very important! If the cache has been cleared we have to inform
	     * the input!
	     */
	    if (clearCache) {
		// Write clear cache command
		writeByte(TC_CLEAR_CACHE);

		// Set clear cache to false
		clearCache = false;
	    }

	    // Set the cache flag
	    tc = Flag.set(tc, CACHE_FLAG);
	}

	// Write type class!
	writeByte(tc);
    }

    protected abstract void writeByteBuffer(ByteBuffer byteBuffer)
	    throws IOException;

    protected abstract void writeCharBuffer(CharBuffer charBuffer)
	    throws IOException;

    protected abstract void writeShortBuffer(ShortBuffer shortBuffer)
	    throws IOException;

    protected abstract void writeIntBuffer(IntBuffer intBuffer)
	    throws IOException;

    protected abstract void writeLongBuffer(LongBuffer longBuffer)
	    throws IOException;

    protected abstract void writeFloatBuffer(FloatBuffer floatBuffer)
	    throws IOException;

    protected abstract void writeDoubleBuffer(DoubleBuffer doubleBuffer)
	    throws IOException;

    public AbstractOStream(CodecRegistry codecSystem, Cache cache,
	    CachePolicy cachePolicy) throws IOException {
	super(codecSystem);

	if (cache == null) {
	    throw new NullPointerException("cache");
	}

	// Save the cache
	this.cache = cache;

	// Set cache policy
	setCachePolicy(cachePolicy);
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Output#writeBuffer(java.nio.Buffer)
     */
    @Override
    public void writeBuffer(Buffer buffer) throws IOException {
	if (buffer == null) {
	    throw new NullPointerException("buffer");
	}

	// Write defined buffer
	writeDefinedBuffer(buffer, getBufferTypeClass(buffer));
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Output#setCachePolicy(com.foxnet.codec.CachePolicy)
     */
    @Override
    public CachePolicy setCachePolicy(CachePolicy cachePolicy) {
	if (cachePolicy == null) {
	    throw new NullPointerException("cachePolicy");
	}

	CachePolicy oldCachePolicy = this.cachePolicy;
	this.cachePolicy = cachePolicy;
	return oldCachePolicy;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Output#getCachePolicy()
     */
    @Override
    public CachePolicy getCachePolicy() {
	return cachePolicy;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Output#cacheSize()
     */
    @Override
    public int cacheSize() {
	return cache.size();
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Output#clearCache()
     */
    @Override
    public void clearCache() {
	// If we have a cache...
	if (cache.size() > 0) {
	    // Clear the cache
	    cache.clear();

	    // We have cleared the cache!
	    clearCache = true;
	}
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Output#getRecursiveDepth()
     */
    @Override
    public int getRecursiveDepth() {
	return recursiveDepth;
    }

    /*
     * (non-Javadoc)
     * @see com.foxnet.codec.Output#writeObject(java.lang.Object)
     */
    @Override
    public void writeObject(Object object) throws Exception {
	try {
	    recursiveDepth++;

	    /*
	     * CHECK NULL
	     */
	    if (object == null) {
		// Write type constant
		writeUncachedTypeClass(TC_NULL);
	    } else {

		// Maybe a primitive type ?
		Integer primitiveObjectId =
			PRIMITIVE_TYPE_CLASSES.get(object.getClass());

		// Check for primitive type
		if (primitiveObjectId != null) {

		    // Just write the primitive object
		    writePrimitiveObject(object, primitiveObjectId);
		} else {

		    // If it is a cache object
		    if (isCacheObject = cachePolicy.isCacheObject(this, object)) {

			// Get index
			int index = cache.lookup(object);

			// Write cache reference
			if (index != -1) {

			    // Write type constant
			    writeUncachedTypeClass(TC_CACHE_REF);

			    // Write the index
			    writeInt(index);

			    // Return
			    return;
			} else {
			    // Put into cache
			    cache.assign(object);
			}
		    }

		    // Check remaining built-in types
		    if (object instanceof String) {
			writeString((String) object);
		    } else if (object.getClass().isArray()) {
			// If 'array' has one dimension and is a primitive type
			if (object.getClass().getComponentType().isPrimitive()) {

			    // Write as primitive array
			    writePrimitiveArray(object,
				    PRIMITIVE_TYPE_CLASSES.get(object
					    .getClass().getComponentType()));
			} else {

			    // Write as object array
			    writeObjectArray(object);
			}
		    } else if (object instanceof Class) {
			writeClass((Class<?>) object);
		    } else if (object instanceof Enum) {
			writeEnum((Enum<?>) object);
		    } else if (object instanceof Buffer) {
			writeBufferObject((Buffer) object);
		    } else {
			writeUnknownObject(object);
		    }
		}
	    }
	} finally {
	    recursiveDepth--;
	}
    }
}